package us.danlib.templating;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * By default, the templator will cache the template file
 *  to save on file system reads. It will check the modified
 *  time stamp on each createTemplate call, however, and 
 *  update the cache if the file has been modified
 *  
 * Caching can be disabled by passing a false as the second
 * 	arg on init
 *  
 * @author Daniel
 *
 */
public class Templator {	
	protected String tplFile;
	
	private boolean caching;
	private String rawBuffer = null;
	private HashMap<String, String> tplVars;
	private long tplFileLastMod = 0;
	
	public Templator(String filename) {
		this(filename, true);
	}
	
	public Templator(String filename, boolean cache) {
		tplFile = filename;
		caching = cache;
		updateCache();
	}
	
	public Template createTemplate(String... globalVars) {
		Template ret = null;
		
		// there must be an even number of args
		if (globalVars.length % 2 == 0) {
			ret = initTemplate();
			
			for (int i=0; i<globalVars.length; i+=2)
				ret.addGlobalVar(globalVars[i], globalVars[i+1]);
		}
				
		return ret;
	}

	/**
	 * @return
	 */
	private Template initTemplate() {
		Template ret;
		if (caching) {
			updateCache();
			ret = new FilledTemplate(rawBuffer, tplVars);
		} else ret = new FilledTemplate(readFile(tplFile), tplVars);
		
		return ret;
	}
	
	/**
	 * Updates the cache if we're caching and
	 *  the file has been changed
	 */
	private void updateCache() {
		if (caching) {
			File theFile = new File(tplFile);
			if (theFile.lastModified() > tplFileLastMod) {				
				rawBuffer = readFile(tplFile);
				tplFileLastMod = theFile.lastModified();
			}
		}	
	}
	
	private String readFile(String filename) {
		StringBuffer ret = new StringBuffer();
		tplVars = new HashMap<String, String>();
		try {
			FileInputStream fstream = new FileInputStream(filename);
			DataInputStream in = new DataInputStream(fstream);
			BufferedReader br = new BufferedReader(new InputStreamReader(in));
			String strLine;
			// check for template vars; this seems stupid
			//  and probably is, but it'll save time when
			//	reading the rest of the template, which could
			//	be important for large files
			while ((strLine = br.readLine()) != null) {
				if (strLine.startsWith("<!-- DEF"))
					setTemplateVar(strLine);
				else {
					ret.append(strLine+"\n");
					break;
				}
			}
			while ((strLine = br.readLine()) != null) 
				ret.append(strLine+"\n");
			
			
			in.close();
		} catch (Exception e) {
			System.err.println("Couldn't load template file: " + filename);
		}
		
		return ret.toString();
	}
	
	/**
	 * Helper function; Add a template var
	 * @param strLine The raw line
	 */
	private void setTemplateVar(String strLine) {
		String[] parts = strLine.substring(9, strLine.length()-4).split("=");
		tplVars.put(parts[0], parts[1]);
	}

	private class FilledTemplate implements Template {
		private String buffer; 
		private HashMap<String, String> classRaws;
		private HashMap<String, LinkedList<String>> filledClasses;
		
		private String currClass;
		private LinkedList<String> currVars;
		private HashMap<String, String> tplVars;
		
		public FilledTemplate(String buffer, HashMap<String, String> tplVars) {
			this.buffer = buffer;
			this.tplVars = tplVars;
			
			// initialize
			classRaws = new HashMap<String, String>();
			filledClasses = new HashMap<String, LinkedList<String>>();
			currVars = new LinkedList<String>();
			
			// find classes
			Pattern p = Pattern.compile(
					"<!-- BEGIN class_([_a-zA-Z]*) -->[\r\n](.+?)[\r\n]<!-- END class_\\1 -->", 
					Pattern.DOTALL+Pattern.MULTILINE);
			Matcher m = p.matcher(buffer);
			while (m.find()) {				
				classRaws.put(m.group(1), m.group(2));
				filledClasses.put(m.group(1), new LinkedList<String>());
			}
			
		}

		@Override
		public void addGlobalVar(String varName, String varValue) {
			buffer = buffer.replaceAll("\\{G_"+varName+"\\}", derefString(varValue));
			Pattern p;
			Matcher m;
			
			// handle IF-else's
			p = Pattern.compile(
					"<!-- IF G_"+varName+" -->[\r\n](.+?)[\r\n]" +
						"(?:<!-- ELSE G_"+varName+" -->.*?)?" +
						"<!-- ENDIF G_"+varName+" -->",
					Pattern.DOTALL+Pattern.MULTILINE);
			m = p.matcher(buffer);

			buffer = m.replaceAll("$1");
		}
		
		@Override
		public void addClass(String className, String...vars) {
			if (!classRaws.containsKey(className))
				return;
			
			String buf = classRaws.get(className);
			if (vars.length % 2 == 0) {
				// there must be an even # of vars
				Pattern p;
				Matcher m;
				for (int i=0; i<vars.length; i+=2) {
					// replace refs
					buf = buf.replaceAll("\\{V_"+vars[i]+"\\}", vars[i+1]);
					
					// handle IF's
					p = Pattern.compile(
							"<!-- IF V_"+vars[i]+" -->(.+?)[\r\n]" +
								"(?:<!-- ELSE V_"+vars[i]+" -->.*?)?" +
								"<!-- ENDIF V_"+vars[i]+" -->",
							Pattern.DOTALL+Pattern.MULTILINE);
					m = p.matcher(buf);
					buf = m.replaceAll("$1");					
				}			
				
				// remove IF's that weren't matched				
				p = Pattern.compile("^<!-- IF V_([_A-Z]*) -->.+?" +
						"(?:<!-- ELSE V_\\1 -->(.*?))?" + 
						"<!-- ENDIF V_\\1 -->$",
						Pattern.DOTALL+Pattern.MULTILINE);
				// lame way to handle nested IFs
				//  properly
				m = p.matcher(buf);				
				do {							
					buf = m.replaceFirst("$2");
					m = p.matcher(buf);
				} while (m.find());
			}
			
			// save the class
			filledClasses.get(className).add(buf);
		}
		
		/**
		 * Clean up a string so it doesn't choke the 
		 * 	regexp replacer
		 * @param src
		 * @return
		 */
		private String derefString(String src) {
			return src.replace("$", "\\$");
		}
		
		@Override
		public String getTemplateVar(String varName) {
			return tplVars.get(varName);
		}
		
		@Override
		public boolean hasTemplateVar(String varName) {
			return tplVars.containsKey(varName);
		}

		@Override
		public String render() {
			StringBuffer classListBuff;
			String output = buffer;
			Pattern p;
			Matcher m;			
			
			// take care of unmatched IF's			
			p = Pattern.compile(
					"<!-- IF G_([A-Z_]*) -->.+?[\r\n]" +
						"(?:<!-- ELSE G_\\1 -->(.*?))?" +
						"<!-- ENDIF G_\\1 -->",
					Pattern.DOTALL+Pattern.MULTILINE);
			// lame way to handle nested IFs
			//  properly
			m = p.matcher(output);				
			do {							
				output = m.replaceFirst("$2");
				m = p.matcher(output);
			} while (m.find());
				
			
			// insert classes
			for (String className : filledClasses.keySet()) {				
				// build the list
				classListBuff = new StringBuffer();				
				for (String item : filledClasses.get(className)) 
					classListBuff.append(item);				
				
				// insert the list
				p = Pattern.compile("<!-- BEGIN class_"+className+" -->" +
						".*<!-- END class_"+className+" -->", Pattern.DOTALL);
				m = p.matcher(output);
				output = m.replaceFirst(derefString(classListBuff.toString()));
			}						
			
			return output;
		}

		@Override
		public void addClassVar(String varName, String varValue) {
			currVars.add(varName);
			currVars.add( derefString(varValue));
		}

		@Override
		public void beginClass(String className) {
			currClass = className;			
		}

		@Override
		public void endClass() {
			addClass(currClass, currVars.toArray(new String[ currVars.size() ]));			
			currVars.clear();
		}
	}
}
